2 Concept of several static snapshots within one epoch¶
Imports¶
import os
os.chdir('../demo')
from pathlib import Path
from IPython.display import Code
from scripts.nb_utils import read_pc, read_from_output_folder, display_xml
import pyhelios
from pyhelios.util.xmldisplayer import find_playback_dir
import numpy as np
import pyvista as pv
pv.set_jupyter_backend('trame')
Example 2.1: Tree moving in the wind during a multi-station TLS acquisition¶
In multi-station TLS acquisitions, movement of trees can result in duplication ("re-occurrence" / "ghosting effects") in the point cloud merged from multiple scans. This can be mimicked using the concept of many static snapshots. We start creating a 3D tree model which we animate in the wind using the Sapling Tree Gen add-on of Blender and its "animation" functionality. We then sample random frames and export the static mesh for this frame of the animation. This can be automatically done with the multi_epoch_b2h Blender add-on in this repository.
The scene¶
We exported 6 static scenes which we will virtually scan from six different positions distributed around the scene. Let's look at one of the scenes.
Code(display_xml('data/scenes/white_birch_005.xml'), language='XML')
<document>
<scene id="scene" name="Scene">
<part>
<filter type="objloader">
<param type="string" key="filepath" value="data\sceneparts\white_birch\leaves_005.obj" />
<param type="string" key="up" value="z" />
</filter>
</part>
<part>
<filter type="objloader">
<param type="string" key="filepath" value="data\sceneparts\white_birch\tree_005.obj" />
<param type="string" key="up" value="z" />
</filter>
</part>
</scene>
</document>
If we plot all the scene parts we exported, we can clearly see that the tree moved. This is what is looks like:
filenames_wood = list(Path('data/sceneparts/white_birch').glob('tree*.obj'))
filenames_leaves = list(Path('data/sceneparts/white_birch').glob('leaves*.obj'))
p = pv.Plotter(notebook=True, off_screen=True)
mesh_wood = pv.read(str(filenames_wood[0]))
a1 = p.add_mesh(mesh_wood, color="saddlebrown")
mesh_leaves = pv.read(str(filenames_leaves[0]))
a2 = p.add_mesh(mesh_leaves, color="forestgreen")
p.camera_position = 'xz'
p.camera.zoom(1.5)
frame = filenames_wood[0].stem[-3:]
t = p.add_text(f"Frame {frame}", color="k")
# Open a gif
p.open_gif("../docs/img/tree_sway.gif")
for _ in range(5): # to make the gif slower
p.write_frame()
# Update mesh and write a frame for each update
for m_wood, m_leaves in zip(filenames_wood[1:], filenames_leaves[1:]):
p.remove_actor(a1)
p.remove_actor(a2)
p.remove_actor(t)
mesh_wood = pv.read(str(m_wood))
mesh_leaves = pv.read(str(m_leaves))
a1 = p.add_mesh(mesh_wood, color="saddlebrown")
a2 = p.add_mesh(mesh_leaves, color="forestgreen")
frame = m_wood.stem[-3:]
t = p.add_text(f"Frame {frame}", color="k")
for _ in range(5): # to make the gif slower
p.write_frame()
# Closes and finalizes movie
p.close()

The survey¶
The corresponding survey looks like this. Note that we are using a custom platforms, which is a TLS tripod with a tilt mount. The scanner is therefore tilted by 90° and scans left to right instead of top to bottom.
Code(display_xml('data/surveys/white_birch_0.xml'), language='XML')
<document>
<scannerSettings id="tls" active="true" pulseFreq_hz="600000" verticalResolution_deg="0.04" horizontalResolution_deg="0.04" />
<survey name="white_birch_0" platform="data/platforms_custom.xml#tripod_tilt_1_of_6" scanner="data/scanners_tls.xml#riegl_vz400" scene="data/scenes/white_birch_029.xml#scene">
<leg>
<platformSettings x="16.0000" y="0.0000" z="0" />
<scannerSettings template="tls" headRotateStart_deg="0.0000" headRotateStop_deg="120.0000" trajectoryTimeInterval_s="0.05" />
</leg>
</survey>
</document>
Executing the simulation¶
!helios data/surveys/white_birch_0.xml -q --lasOutput --zipOutput --rebuildScene
!helios data/surveys/white_birch_1.xml -q --lasOutput --zipOutput --rebuildScene
!helios data/surveys/white_birch_2.xml -q --lasOutput --zipOutput --rebuildScene
!helios data/surveys/white_birch_3.xml -q --lasOutput --zipOutput --rebuildScene
!helios data/surveys/white_birch_4.xml -q --lasOutput --zipOutput --rebuildScene
!helios data/surveys/white_birch_5.xml -q --lasOutput --zipOutput --rebuildScene
# reading and merging
output_path = Path(find_playback_dir('data/surveys/white_birch_0.xml'))
pc_t1, _, classification_t1, helios_amplitude_t1, _, pt_src_id_t1 = read_pc(output_path / 'leg000_points.laz', 0)
output_path = Path(find_playback_dir('data/surveys/white_birch_1.xml'))
pc_t2, _, classification_t2, helios_amplitude_t2, _, pt_src_id_t2 = read_pc(output_path / 'leg000_points.laz', 1)
output_path = Path(find_playback_dir('data/surveys/white_birch_2.xml'))
pc_t3, _, classification_t3, helios_amplitude_t3, _, pt_src_id_t3 = read_pc(output_path / 'leg000_points.laz', 2)
output_path = Path(find_playback_dir('data/surveys/white_birch_3.xml'))
pc_t4, _, classification_t4, helios_amplitude_t4, _, pt_src_id_t4 = read_pc(output_path / 'leg000_points.laz', 3)
output_path = Path(find_playback_dir('data/surveys/white_birch_4.xml'))
pc_t5, _, classification_t5, helios_amplitude_t5, _, pt_src_id_t5 = read_pc(output_path / 'leg000_points.laz', 4)
output_path = Path(find_playback_dir('data/surveys/white_birch_5.xml'))
pc_t6, _, classification_t6, helios_amplitude_t6, _, pt_src_id_t6 = read_pc(output_path / 'leg000_points.laz', 5)
pc = pc = np.vstack([pc_t1, pc_t2, pc_t3, pc_t4, pc_t5, pc_t6])
classification = np.hstack([classification_t1, classification_t2, classification_t3, classification_t4, classification_t5, classification_t6])
helios_amplitude = np.hstack([helios_amplitude_t1, helios_amplitude_t2, helios_amplitude_t3, helios_amplitude_t4, helios_amplitude_t5, helios_amplitude_t6])
pt_src_id = np.hstack([pt_src_id_t1, pt_src_id_t2, pt_src_id_t3, pt_src_id_t4, pt_src_id_t5, pt_src_id_t6 ])
Visualizing the output¶
We are plotting the output coloured by the "Point Source ID", i.e., the ID of the scan position that the scan originates from (left), and coloured by classification (right; 0 = wood, 1 = leaf).
p = pv.Plotter(notebook=True, shape=(1, 2))
p.add_points(pc[::3],
scalars=pt_src_id[::3].astype('int32'),
style='points',
#cmap='gwv',
point_size=2,
scalar_bar_args={'title': 'Point Source ID',
'n_labels': 6,
'position_x': 0.3})
p.subplot(0, 1)
p.add_points(pc[::3],
scalars=classification[::3],
style='points',
cmap=['saddlebrown', 'forestgreen'],
render_points_as_spheres=True,
point_size=2,
scalar_bar_args={'title': 'Classification',
'n_labels': 2,
'position_x': 0.3})
p.link_views()
p.camera_position = 'xz'
p.camera.zoom(3)
p.show_axes()
p.show()